// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:kernel/ast.dart';
import 'package:kernel/target/targets.dart';
import 'package:kernel/type_environment.dart';

import '../base/problems.dart';
import '../codes/cfe_codes.dart';
import 'constant_evaluator.dart';

typedef ReportErrorFunction =
    void Function(LocatedMessage message, List<LocatedMessage>? context);

class TryConstantEvaluator extends ConstantEvaluator {
  final bool _supportReevaluationForTesting;

  @override
  final _ErrorReporter errorReporter;

  TryConstantEvaluator(
    DartLibrarySupport librarySupport,
    ConstantsBackend constantsBackend,
    Component component,
    TypeEnvironment typeEnvironment,
    ReportErrorFunction reportError, {
    Map<String, String>? environmentDefines,
    bool supportReevaluationForTesting = false,
  }) : this._(
         librarySupport,
         constantsBackend,
         component,
         typeEnvironment,
         new _ErrorReporter(reportError),
         environmentDefines: environmentDefines,
         supportReevaluationForTesting: supportReevaluationForTesting,
       );

  TryConstantEvaluator._(
    DartLibrarySupport librarySupport,
    ConstantsBackend constantsBackend,
    Component component,
    TypeEnvironment typeEnvironment,
    this.errorReporter, {
    Map<String, String>? environmentDefines,
    bool supportReevaluationForTesting = false,
  }) : _supportReevaluationForTesting = supportReevaluationForTesting,
       super(
         librarySupport,
         constantsBackend,
         component,
         environmentDefines ?? const {},
         typeEnvironment,
         errorReporter,
         enableTripleShift: true,
       );

  @override
  // Coverage-ignore(suite): Not run.
  Constant evaluate(
    StaticTypeContext staticTypeContext,
    Expression node, {
    TreeNode? contextNode,
  }) {
    return evaluateOrNull(staticTypeContext, node, contextNode: contextNode)!;
  }

  /// Evaluates [node] to a constant in the given [staticTypeContext].
  ///
  /// If [requireConstant] is `true`, an error is reported if [node] is not
  /// a valid constant. Otherwise, returns `null` if [node] is not a valid
  /// constant.
  Constant? evaluateOrNull(
    StaticTypeContext staticTypeContext,
    Expression node, {
    TreeNode? contextNode,
    bool requireConstant = true,
  }) {
    errorReporter.requiresConstant = requireConstant;
    if (node is ConstantExpression) {
      // Coverage-ignore-block(suite): Not run.
      Constant constant = node.constant;
      // TODO(fishythefish): Add more control over what to do with
      // [UnevaluatedConstant]s.
      if (constant is UnevaluatedConstant) {
        Constant result = super.evaluate(
          staticTypeContext,
          constant.expression,
          contextNode: contextNode,
        );
        assert(
          result is UnevaluatedConstant ||
              !(new UnevaluatedConstantFinder().visitConstant(result)),
          "Invalid constant result $result from ${constant.expression}.",
        );
        if (!_supportReevaluationForTesting) {
          node.constant = result;
        }
        return result;
      }
      return constant;
    }
    if (requireConstant) {
      // Coverage-ignore-block(suite): Not run.
      return super.evaluate(staticTypeContext, node, contextNode: contextNode);
    } else {
      Constant constant = super.evaluate(
        staticTypeContext,
        node,
        contextNode: contextNode,
      );
      if (constant is UnevaluatedConstant &&
          constant.expression is InvalidExpression) {
        return null;
      }
      return constant;
    }
  }
}

class _ErrorReporter implements ErrorReporter {
  final ReportErrorFunction _reportError;
  late bool requiresConstant;

  _ErrorReporter(this._reportError);

  @override
  // Coverage-ignore(suite): Not run.
  bool get supportsTrackingReportedErrors => false;

  @override
  // Coverage-ignore(suite): Not run.
  bool get hasSeenError {
    return unsupported("_ErrorReporter.hasSeenError", -1, null);
  }

  @override
  void report(LocatedMessage message, [List<LocatedMessage>? context]) {
    if (requiresConstant) {
      // Coverage-ignore-block(suite): Not run.
      _reportError(message, context);
    }
  }
}

// Coverage-ignore(suite): Not run.
/// [Constant] visitor that returns `true` if the visitor constant contains
/// an [UnevaluatedConstant].
class UnevaluatedConstantFinder extends ComputeOnceConstantVisitor<bool> {
  UnevaluatedConstantFinder();

  @override
  bool visitUnevaluatedConstant(UnevaluatedConstant node) => true;

  @override
  bool visitInstantiationConstant(InstantiationConstant node) {
    return visitConstant(node.tearOffConstant);
  }

  @override
  bool visitInstanceConstant(InstanceConstant node) {
    for (Constant value in node.fieldValues.values) {
      if (visitConstant(value)) {
        return true;
      }
    }
    return false;
  }

  @override
  bool visitSetConstant(SetConstant node) {
    for (Constant value in node.entries) {
      if (visitConstant(value)) {
        return true;
      }
    }
    return false;
  }

  @override
  bool visitListConstant(ListConstant node) {
    for (Constant value in node.entries) {
      if (visitConstant(value)) {
        return true;
      }
    }
    return false;
  }

  @override
  bool visitMapConstant(MapConstant node) {
    for (ConstantMapEntry entry in node.entries) {
      if (visitConstant(entry.key)) {
        return true;
      }
      if (visitConstant(entry.value)) {
        return true;
      }
    }
    return false;
  }

  @override
  bool visitRecordConstant(RecordConstant node) {
    for (Constant c in node.positional) {
      if (visitConstant(c)) return true;
    }
    for (Constant c in node.named.values) {
      if (visitConstant(c)) return true;
    }
    return false;
  }

  @override
  bool visitBoolConstant(BoolConstant node) => false;

  @override
  bool visitConstructorTearOffConstant(ConstructorTearOffConstant node) =>
      false;

  @override
  bool visitDoubleConstant(DoubleConstant node) => false;

  @override
  bool visitIntConstant(IntConstant node) => false;

  @override
  bool visitNullConstant(NullConstant node) => false;

  @override
  bool visitRedirectingFactoryTearOffConstant(
    RedirectingFactoryTearOffConstant node,
  ) => false;

  @override
  bool visitStaticTearOffConstant(StaticTearOffConstant node) => false;

  @override
  bool visitStringConstant(StringConstant node) => false;

  @override
  bool visitSymbolConstant(SymbolConstant node) => false;

  @override
  bool visitTypeLiteralConstant(TypeLiteralConstant node) => false;

  @override
  bool visitTypedefTearOffConstant(TypedefTearOffConstant node) => false;

  @override
  bool visitAuxiliaryConstant(AuxiliaryConstant node) {
    throw new UnsupportedError(
      "Unsupported auxiliary constant ${node} (${node.runtimeType}).",
    );
  }
}
